Introduction
Welcome!
Code Examples
Day 1
Challenge: KISS
Solution: KISS
Day 2
Challenge: Type Annotations
Solution: Type Annotations
Day 3
Challenge: Decoupling
Solution: Decoupling
Day 4
Challenge: DRY
Solution: DRY
Day 5
Challenge: String Formatting
Solution: String Formatting
Day 6
Challenge: Law Of Demeter
Solution: Law Of Demeter
Day 7
Challenge: Better Discounts
Solution: Better Discounts
Day 8
Challenge: Payment Strategy
Solution: Payment Strategy
Day 9
Challenge: Plugins
Solution: Plugins
Day 10
Challenge: Object Oriented To Functional
Solution: Object Oriented To Functional
Day 11
Challenge: Cohesion
Solution: Cohesion
Day 12
Challenge: MVP
Solution: MVP
Day 13
Challenge: Inheritance
Solution: Inheritance
Day 14
Challenge: Abstraction
Solution: Abstraction
Day 15
Challenge: Higher-Order Functions
Solution: Higher-Order Functions
Day 16
Challenge: Configuration
Solution: Configuration
Day 17
Challenge: Concurrency
Solution: Concurrency
Day 18
Challenge: Refactoring
Solution: Refactoring
Day 19
Challenge: Itertools
Solution: Itertools
Day 20
Challenge: Inappropriate Intimacy
Solution: Inappropriate Intimacy
Wrap Up
End of Part 1

Here my solution, assuming only 1 discount can be applied to the cart.
from dataclasses import dataclass, field
from decimal import Decimal
class ItemNotFoundException(Exception):
pass
@dataclass
class Item:
name: str
price: Decimal
quantity: int
@property
def subtotal(self) -> Decimal:
return self.price * self.quantity
@dataclass
class Discount:
code: str = ""
type: str = ""
amount: Decimal = Decimal(0)
DISCOUNTS = {
"SAVE10": Discount(code="SAVE10", type="percentage", amount=Decimal("0.1")),
"5BUCKSOFF": Discount(code="5BUCKSOFF", type="fixed", amount=Decimal("5.00") ),
"FREESHIPPING": Discount(code="FREESHIPPING", type="fixed", amount=Decimal("2.00")),
"BLKFRIDAY": Discount(code="BLKFRIDAY", type="percentage", amount=Decimal("0.2"))
}
@dataclass
class ShoppingCart:
items: list[Item] = field(default_factory = list)
disct: Discount = field(default_factory = lambda: Discount())
def add_item(self, item: Item) -> None:
self.items.append(item)
def remove_item(self, item_name: str) -> None:
found_item = self.find_item(item_name)
self.items.remove(found_item)
def find_item(self, item_name: str) -> Item:
for item in self.items:
if item.name == item_name:
return item
raise ItemNotFoundException(f"Item '{item_name}' not found.")
@property
def subtotal(self) -> Decimal:
return Decimal(sum(item.subtotal for item in self.items))
@property
def discount(self) -> Decimal:
if self.disct.code == "" or self.disct.code not in DISCOUNTS:
return Decimal("0")
else:
if DISCOUNTS[self.disct.code].type == "percentage":
return self.subtotal * DISCOUNTS[self.disct.code].amount
elif DISCOUNTS[self.disct.code].type == "fixed":
return DISCOUNTS[self.disct.code].amount
else:
return Decimal("0")
@property
def total(self) -> Decimal:
return self.subtotal - self.discount
def display(self) -> None:
# Print the cart
print("Shopping Cart:")
print(f"{'Item':<10}{'Price':>10}{'Qty':>7}{'Total':>13}")
for item in self.items:
print(
f"{item.name:<12}${item.price:>7.2f}{item.quantity:>7} ${item.subtotal:>7.2f}"
)
print("=" * 40)
print(f"Subtotal: ${self.subtotal:>7.2f}")
print(f"Discount: ${self.discount:>7.2f}")
print(f"Total: ${self.total:>7.2f}")
def main() -> None:
# Create a shopping cart and add some items to it
cart = ShoppingCart(
items=[
Item("Apple", Decimal("1.50"), 10),
Item("Banana", Decimal("2.00"), 2),
Item("Pizza", Decimal("11.90"), 5),
],
disct = Discount("5BUCKSOFF")
)
cart.display()
if __name__ == "__main__":
main()
This is a nice solution! However, the assumption is not correct. A cart should be able to have multiple discounts, try updating the code so it can handle those scenarios as well :)
Thanks Andreas for your suggestion. Here is the updated code that supports multiple discounts:
from dataclasses import dataclass, field
from decimal import Decimal
class ItemNotFoundException(Exception):
pass
@dataclass
class Item:
name: str
price: Decimal
quantity: float
@property
def subtotal(self) -> Decimal:
return self.price * self.quantity
@dataclass
class Discount:
type: str = ""
amount: Decimal = Decimal(0)
DISCOUNTS = {
"SAVE10": Discount(type="percentage", amount=Decimal("0.1")),
"5BUCKSOFF": Discount(type="fixed", amount=Decimal("5.00") ),
"FREESHIPPING": Discount(type="fixed", amount=Decimal("2.00")),
"BLKFRIDAY": Discount(type="percentage", amount=Decimal("0.2"))
}
@dataclass
class ShoppingCart:
items: list[Item] = field(default_factory = list)
disct: list[str] = field(default_factory = list)
def add_item(self, item: Item) -> None:
self.items.append(item)
def remove_item(self, item_name: str) -> None:
found_item = self.find_item(item_name)
self.items.remove(found_item)
def find_item(self, item_name: str) -> Item:
for item in self.items:
if item.name == item_name:
return item
raise ItemNotFoundException(f"Item '{item_name}' not found.")
@property
def subtotal(self) -> Decimal:
return Decimal(sum(item.subtotal for item in self.items))
@property
def discount(self) -> Decimal:
total_discount = Decimal("0")
for code in self.disct:
if code in DISCOUNTS:
if DISCOUNTS[code].type == "percentage":
total_discount += DISCOUNTS[code].amount * self.subtotal
elif DISCOUNTS[code].type == "fixed":
total_discount += DISCOUNTS[code].amount
return total_discount
@property
def total(self) -> Decimal:
return self.subtotal - self.discount
def display(self) -> None:
# Print the cart
print("Shopping Cart:")
print(f"{'Item':<10}{'Price':>10}{'Qty':>7}{'Total':>13}")
for item in self.items:
print(
f"{item.name:<12}${item.price:>7.2f}{item.quantity:>7} ${item.subtotal:>7.2f}"
)
print("=" * 40)
print(f"Subtotal: ${self.subtotal:>7.2f}")
print(f"Discount: ${self.discount:>7.2f}")
print(f"Total: ${self.total:>7.2f}")
def main() -> None:
# Create a shopping cart and add some items to it
cart = ShoppingCart(
items=[
Item("Apple", Decimal("1.50"), 10),
Item("Banana", Decimal("2.00"), 2),
Item("Pizza", Decimal("11.90"), 5),
],
disct = ["SAVE10","FREESHIPPING","5BUCKSOFF"]
)
cart.display()
if __name__ == "__main__":
main()
Great! Looks good, nice solution :D
A little bit of scope creep, because it's the weekend and why not? :)
We might not want to allow discounting more than the subtotal, so potentially the discount property in ShoppingCart in the solution could return something like this instead:
return total_discount if total_discount <= self.subtotal else self.subtotal
Hi Raegan, good idea (if you like large discounts ;) ).
Looking from your video I think I overcomplicated my solution :)
I wrote an Enum for the discounts and their value, two functions for flat or percentage discounts and then a "strategy" function that link a specific DiscountCode to flat or percentage discount.
Two drawbacks I found are:
- I need to add to the Enum class and the strategy function every new discount code
- On the flat discount I had to add the total, although it is not used, but was needed to use a compact function in the strategy function.
Code below
def percentage_discount(discount: Decimal, total: Decimal) -> Decimal:
return total * discount
def flat_discount(discount: Decimal, total: Decimal) -> Decimal:
return discount
class DiscountCode(Enum):
SAVE10 = Decimal(0.1)
FIVEBUCKSOFF = Decimal(5)
FREESHIPPING = Decimal(2)
BLACKFRIDAY = Decimal(0.2)
DEFAULT = Decimal(0)
DiscountFn = Callable[[Decimal], Decimal]
def discount_strategy(code: DiscountCode) -> DiscountFn:
mapping = {
DiscountCode.BLACKFRIDAY: percentage_discount,
DiscountCode.SAVE10: percentage_discount,
DiscountCode.FIVEBUCKSOFF: flat_discount,
DiscountCode.FREESHIPPING: flat_discount,
DiscountCode.DEFAULT: flat_discount,
}
return partial(
mapping.get(code, mapping[DiscountCode.DEFAULT]), discount=code.value
)
@dataclass
class ShoppingCart:
items: list[Item] = field(default_factory=list)
discounts: list[DiscountCode] = field(default_factory=list)
def apply_discount(self, discount: DiscountCode) -> None:
self.discounts.append(discount)
[...]
@property
def discount(self) -> Decimal:
discount = Decimal(0)
for discount_code in self.discounts:
discount_fn = discount_strategy(discount_code)
discount += discount_fn(total=self.subtotal)
return discount
[...]
Thanks for posting your code! It's something that I notice myself as well when writing code: initially I start writing code that's too complex, but then it serves as a great starting point for refactoring it into something much simpler. It seems that I need to write the complex code first to help me understand the problem before I can come up with a better, simpler solution.
Hi Arjan, I like your solution! One difference I noticed in my code was making the Discount object "callable". However, I do think allowing a list of discounts is a better solution.
class DiscountType(enum.Enum):
VALUE = enum.auto()
PERCENTAGE = enum.auto()
@dataclass
class Discount:
amount: Decimal
type: DiscountType
def __call__(self, total: Decimal) -> Decimal:
if self.type == DiscountType.VALUE:
return total - self.amount
return total * (1 - self.amount)
DISCOUNT_MAP = {
"SAVE10": Discount(Decimal("0.1"), DiscountType.PERCENTAGE),
"5BUCKSOFF": Discount(Decimal("5.00"), DiscountType.VALUE),
"FREESHIPPING": Discount(Decimal("2.00"), DiscountType.VALUE),
"BLKFRIDAY": Discount(Decimal("0.2"), DiscountType.PERCENTAGE),
}
def discount_factory(code: str) -> Discount:
discount = DISCOUNT_MAP.get(code, None)
if discount is None:
raise NameError(f"{code} is not a valid discount code")
return discount
@dataclass
class ShoppingCart:
items: list[Item] = field(default_factory=list)
discount: Discount | None = None
@property
def total(self) -> Decimal:
if self.discount is None:
return self.subtotal
return self.discount(self.subtotal)
Hi Mark, that's also a nice solution!
I used a dict too, but mapping strings (discount codes) to functions that apply discounts. In a sense, I don't need a Discount class, which might make the code a little simpler, but you convinced me in your video when you said that we could read the "amount" and "percentage" from a database. We can't use that with functions.
For those interested:
GetDiscountFn = typing.Callable[[Decimal], Decimal]
def get_discount(base_price: Decimal, discount_code: str) -> Decimal:
DISCOUNT_FNS: dict[str, GetDiscountFn] = {
"SAVE10": discount_five_bucks,
"5BUCKSOFF": discount_ten_percent,
"FREESHIPPING": discount_free_shipping,
"BLKFRIDAY": discount_black_friday,
}
if discount_code not in DISCOUNT_FNS:
raise UnknownDiscountCode(discount_code)
discount_fn = DISCOUNT_FNS[discount_code]
return discount_fn(base_price)
Took a similar approach. what i like about the Callable approach is that it further decouples Discounts from ShoppingCarts. ShoppingCart doesn't need to know anything about how discounts are applied, only that it can get a discount value when it applies that function on the cart subtotal.
It also allows to support non-linear discounts (e.g. a tier-based discount that looks like a step function, depending on some subtotal thresholds).
Agreed! It is a very useful and elegant way of solving the challenge!
Same here, I implemented the discounts in a dict with Callables:
AbsolutDiscountCalculation = Callable[[Decimal],Decimal]
discounts: dict[str,AbsolutDiscountCalculation] = field(default_factory=dict)
which are finally just added as lambda functions which are of course not reusable in this way:
discounts={
"SAVE10": lambda subtotal: subtotal * Decimal("0.1"),
"5BUCKSOFF": lambda _: Decimal("5.00"),
"FREESHIPPING": lambda _: Decimal("2.00"),
"BLKFRIDAY": lambda subtotal: subtotal * Decimal("0.2"),
},
@property
def discount(self) -> Decimal:
if self.discount_code in self.discounts:
return self.discounts[self.discount_code](self.subtotal)
else:
return Decimal("0.00")
Inspired by a current task in the job, where a similar approach is necessary to implement more or less complicated calculations based on a key, like Sylvain mentioned.
Nice to see some lambda functions!